﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using Skugo.Shared;

namespace Skugo
{
    public delegate Boolean PropertyCheckFn(MemberInfo member);
    public delegate Object GetPropertyFn(Object owner);
    public delegate void SetPropertyFn(Object owner, Object value);
    public delegate String[] EnumerateChildTypesFn();
    public delegate void AddChildFn(Object owner, Int32 enumeratedIndex);
    public delegate void RemoveChildFn(Int32 index);


    public struct PropertyProviderInfo
    {
        /// <summary>
        /// The root object that we're building the tree for. Note that sometimes
        /// trees can just be based on type, or they can actually use the virtual type
        /// of particular derived members of an object.
        /// </summary>
        public Object RootObject;

        /// <summary>
        /// A delegate that we call to check if a particular member is valid to be added to the tree.
        /// </summary>
        public PropertyCheckFn IsMemberValid;
    }

    /// <summary>
    /// Responsible for providing properties from a given object.
    /// </summary>
    public interface IPropertyProvider
    {
        /// <summary>
        /// Generate a property tree from the given information.
        /// </summary>
        /// <param name="info">All the information we need for building the tree.</param>
        /// <returns>The root node of the property tree.</returns>
        LocalPropertyNode GetPropertyTree(PropertyProviderInfo info);
    }

    public class GeneralPropertyProvider : IPropertyProvider
    {
        /// <summary>
        /// The default flags we use for all reflected calls.
        /// </summary>
        private const BindingFlags DefaultFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;

        public virtual LocalPropertyNode GetPropertyTree(PropertyProviderInfo info)
        {
            // Create the root node for the tree
            var root = new LocalPropertyNode();
            root.Kind = NodeKind.Expandable;

            // Populate the root node (and all child/subchild nodes)
            this.PopulateMembers(root, info.RootObject, info.IsMemberValid);

            // Return the root node
            return root;
        }

        private static void SetNameDescriptionCategory(LocalPropertyNode node, MemberInfo member)
        {
            // Set the optional name
            node.OptionalName = member.Name;

            // Get an array of all property attributes
            var attributes = member.GetCustomAttributes(typeof(PropertyAttribute), true);

            // Loop through all the property attributes
            foreach (PropertyAttribute attribute in attributes)
            {
                // Set whether this node should by default be expanded or not
                node.DefaultExpanded = attribute.Expanded;

                // If the node doesn't have a category set yet...
                if (node.OptionalCategory == null)
                {
                    // Set the category
                    node.OptionalCategory = attribute.Category;
                }

                // If the node doesn't have a description set yet...
                if (node.OptionalCategory == null)
                {
                    // Set the description
                    node.OptionalDescription = attribute.Description;
                }
            }
        }

        private void AddFlags(LocalPropertyNode parent, MemberInfo member, Type type, GetPropertyFn getter, SetPropertyFn setter)
        {
            // Loop through all the values in the enum
            foreach (var value in Enum.GetValues(type))
            {
                // We only want to consider flags that have exactly one bit turned on
                // These flags will always be a power of two
                var flag = (Int32)value;

                // Compute the flag power
                var powerOfTwo = Math.Log(flag, 2);

                // If the flag is exactly a power of two (whole number)
                if (flag != 0 && powerOfTwo == Math.Round(powerOfTwo))
                {
                    // Create a node to represent this flag
                    var node = new LocalPropertyNode();

                    // Even though the node is not exaclty a primitive, it will be treated
                    // as a boolean primitive that just, underlying, sets the flags
                    node.Kind = NodeKind.Primitive;

                    // Set the parent node
                    node.Parent = parent;

                    // Setup the basic description properties of the node
                    SetNameDescriptionCategory(node, member);

                    // We want to overwrite the name of this node to not be the name of the member,
                    // but rather the name of the actual enum flag
                    node.OptionalName = Enum.GetName(type, value);

                    // We special case each flag to be treated as a boolean
                    node.Type = typeof(Boolean);

                    // Store the getter and setter on the node
                    node.Getter = (obj) =>
                    {
                        // We get the actual value of the enum
                        var currentValue = (Int32)getter(obj);
                        // Then we check if the current flag is on (resulting in a bool)
                        return (currentValue & flag) != 0;
                    };

                    node.Setter = (obj, val) =>
                    {
                        // Figure out whether we're toggling the flag on or off
                        var toggle = (Boolean)val;
                        // We get the actual value of the enum
                        var currentValue = (Int32)getter(obj);

                        // There's probably a way to do this without an if-check, but I don't want to think right now
                        if (toggle)
                        {
                            // Turn the flag on
                            setter(obj, currentValue | flag);
                        }
                        else
                        {
                            // Turn the flag off
                            setter(obj, currentValue & ~flag);
                        }
                    };

                    // Add this new node as a child
                    parent.Children.Add(node);
                }
            }
        }

        protected void AddObjectBranch(LocalPropertyNode parent, MemberInfo member, Type type, Object parentObject, GetPropertyFn getter, SetPropertyFn setter, PropertyCheckFn memberCheck)
        {
            // Get the current value of the branch
            var obj = getter(parentObject);

            // We handle the case of flags enums as a special case
            if (type.IsEnum && type.GetCustomAttributes(typeof(FlagsAttribute), true).Length != 0)
            {
                // Handle the flags and exit out early
                this.AddFlags(parent, member, type, getter, setter);
                return;
            }

            // Create a node to represent this member
            var node = new LocalPropertyNode();

            // The node can only be a primitive, expandable, or enumerable (we handle methods elsewhere)
            // If the type given for this node is a primitive...
            if (TypeFactory.IsPrimitive(type))
            {
                node.Kind = NodeKind.Primitive;
            }
            // If the type is registered as an enumerable type...
            else if (TypeFactory.IsEnumerable(type))
            {
                // The node is an enumerable
                node.Kind = NodeKind.Enumerable;

                // Enumerate our options and store it on the node
                node.EnumeratedOptions = TypeFactory.GetInfoFromEnumerableType(type).Enumerate();
            }
            // Otherwise, the type must be a regular old expandable type!
            else
            {
                node.Kind = NodeKind.Expandable;
            }

            // Set the parent node
            node.Parent = parent;

            // Store the name of the node
            SetNameDescriptionCategory(node, member);

            // Set the name of the type on the node
            node.Type = type;

            // Store the getter and setter on the node
            node.Getter = getter;
            node.Setter = setter;

            // Add this new node as a child
            parent.Children.Add(node);

            // Perform a query to see if the type implements the generic 'ICollection<>' interface
            var collectionType = type.GetInterfaces().FirstOrDefault(x =>
              x.IsGenericType &&
              x.GetGenericTypeDefinition() == typeof(ICollection<>));

            // AS long as the object is not a null object...
            if (obj != null)
            {
                // Is the type is a collection type...
                if (collectionType != null)
                {
                    // Collections are always expandable
                    node.Kind = NodeKind.Expandable;

                    // Get the generic arguments for the ICollection<> type (there should only be one)
                    var arguments = collectionType.GetGenericArguments();

                    // With collections, we allow addition and removal of the type
                    node.AddChild = (owner, enumeratedIndex) =>
                    {
                        // Create a new instance of the item that the collection contains
                        var newItem = Activator.CreateInstance(arguments[0]);

                        // Add that item to the collection
                        collectionType.GetMethod("Add").Invoke(owner, new Object[] { newItem });
                    };

                    // The local property tree can store the value because we know the tree will update if the value is changed
                    // Maybe instead of storing the last value, we'll just query the value for that property node at the given time
                    // We just say we removed a flattened index, and then we leave it to the propgrid to re-query the tree for the element
                    // That means remove actually uses the element
                    // That will actually change the major problem to being part of how we populate child nodes of a collection

                    this.PopulateMembers(node, obj, memberCheck);
                }
                else
                {
                    // Populate the members (we should really only need to do this in the expandable case)
                    this.PopulateMembers(node, obj, memberCheck);
                }
            }
        }

        protected void AddMethodLeaf(LocalPropertyNode parent, MethodInfo method)
        {
            // This is an object, so create an object node...
            var node = new LocalPropertyNode();
            node.Kind = NodeKind.Method;

            // Set the parent node
            node.Parent = parent;

            // Store the name of the method
            SetNameDescriptionCategory(node, method);

            // Store the method info on the node
            node.Method = method;

            // Add this new node as a child
            parent.Children.Add(node);
        }

        protected void PopulateCollection(LocalPropertyNode current, Type type, Type collectionType, Object value)
        {

        }

        protected void PopulateMembers(LocalPropertyNode current, Object obj, PropertyCheckFn memberCheck)
        {
            // Get the type of the object
            var type = obj.GetType();

            // Methods
            {
                // Get all the members of the object (note that we're just trying to get any member marked with the Property attribute, even statics)
                var members = type.GetMethods(DefaultFlags);

                // Loop through all the reflected members
                foreach (var member in members)
                {
                    // If the member was valid to be considered in the tree (generally by some attribute)
                    if (memberCheck(member))
                    {
                        // Add the method leaf node
                        this.AddMethodLeaf(current, member);
                    }
                }
            }

            // Fields
            {
                // Get all the members of the object (note that we're just trying to get any member marked with the Property attribute, even statics)
                var members = type.GetFields(DefaultFlags);

                // Loop through all the reflected members
                foreach (var member in members)
                {
                    // If the member was valid to be considered in the tree (generally by some attribute)
                    if (memberCheck(member))
                    {
                        // Make a local copy of the member for the below closures
                        var localMember = member;

                        // Add an object branch (this member could have children, or could be a leaf value itself)
                        this.AddObjectBranch(current,
                            member,
                            member.FieldType,
                            obj,
                            (owner) => localMember.GetValue(owner),
                            (owner, value) => localMember.SetValue(owner, value),
                            memberCheck);
                    }
                }
            }

            // Properties
            {
                // Get all the members of the object (note that we're just trying to get any member marked with the Property attribute, even statics)
                var members = type.GetProperties(DefaultFlags);

                // Loop through all the reflected members
                foreach (var member in members)
                {
                    // If the member was valid to be considered in the tree (generally by some attribute)
                    if (memberCheck(member))
                    {
                        // Make a local copy of the member for the below closures
                        var localMember = member;

                        // Add an object branch (this member could have children, or could be a leaf value itself)
                        this.AddObjectBranch(current,
                            member,
                            member.PropertyType,
                            obj,
                            (owner) => localMember.GetValue(owner, null),
                            (owner, value) => localMember.SetValue(owner, value, null),
                            memberCheck);
                    }
                }
            }
        }
    }
}
